
AceGUI = LibStub("AceGUI-3.0")
local AceGUI = AceGUI
local GetMobType = OneHitWonder.GetMobType
local mobdb
local f = format

--------------------------
-- Label 	 			--
--------------------------
do
	local Type = "SimpleHTML"
	local Version = 1
	
	local function OnAcquire(self)
		self:SetText("")
		self.frame:SetHeight(10)
		self.frame:SetWidth(200)
	end
	
	local function OnRelease(self)
		self.frame:ClearAllPoints()
		self.frame:Hide()
	end
	
	local function SetText(self, text)
		self.frame:SetText(text or "")
	end

	local function SetWidth(self, w)
		self.frame:SetWidth(w)
	end
	
	local function Constructor()
		local frame = CreateFrame("SimpleHTML",nil,UIParent)
		local self = {}
		self.type = Type
		
		self.OnRelease = OnRelease
		self.OnAcquire = OnAcquire
		self.SetText = SetText
		self.SetWidth = SetWidth
		self.frame = frame
		frame.obj = self
		
		frame:SetHeight(10)
		frame:SetWidth(200)
		frame:SetFontObject(GameFontHighlightSmall)
		
		AceGUI:RegisterAsWidget(self)
		return self
	end
	
	AceGUI:RegisterWidgetType(Type,Constructor,Version)
end




----------------
-- Simple Main Frame --
----------------
--[[
    Events :
        OnClose

]]
do
   local Type = "SimpleFrame"
   local Version = 1
   
   local FrameBackdrop = {
      bgFile="Interface/Tooltips/UI-Tooltip-Background",
      edgeFile="Interface/Tooltips/UI-Tooltip-Border", 
      tile = true, tileSize = 16, edgeSize = 16, 
      insets = { left = 4, right = 4, top = 4, bottom = 4 }
   }
   
   local function frameOnClose(this)
      this.obj:Fire("OnClose")
   end
   
   local function closeOnClick(this)
      this.obj:Hide()
   end
   
   local function frameOnMouseDown(this)
		this:StartMoving()
      AceGUI:ClearFocus()
   end
   
   local function titleOnMouseDown(this)
      this:GetParent():StartMoving()
      AceGUI:ClearFocus()
   end
   
   local function frameOnMouseUp(this)
      local frame = this
      frame:StopMovingOrSizing()
      local self = frame.obj
      local status = self.status or self.localstatus
      status.width = frame:GetWidth()
      status.height = frame:GetHeight()
      status.top = frame:GetTop()
      status.left = frame:GetLeft()
   end
   
   local function Hide(self)
      self.frame:Hide()
   end
   
   local function Show(self)
      self.frame:Show()
   end
   
   local function OnAcquire(self)
      self.frame:SetParent(UIParent)
      self.frame:SetFrameStrata("FULLSCREEN_DIALOG")
      self:ApplyStatus()
   end
   
   local function OnRelease(self)
      self.status = nil
      for k in pairs(self.localstatus) do
         self.localstatus[k] = nil
      end
   end
   
   -- called to set an external table to store status in
   local function SetStatusTable(self, status)
      assert(type(status) == "table")
      self.status = status
      self:ApplyStatus()
   end
   
   local function ApplyStatus(self)
      local status = self.status or self.localstatus
      local frame = self.frame
      self:SetWidth(status.width or 100)
      self:SetHeight(status.height or 100)
      if status.top and status.left then
         frame:SetPoint("TOP",UIParent,"BOTTOM",0,status.top)
         frame:SetPoint("LEFT",UIParent,"LEFT",status.left,0)
      else
         frame:SetPoint("CENTER",UIParent,"CENTER")
      end
   end
   
   local function OnWidthSet(self, width)
      local content = self.content
      local contentwidth = width - 34
      if contentwidth < 0 then
         contentwidth = 0
      end
      content:SetWidth(contentwidth)
      content.width = contentwidth
   end
   
   
   local function OnHeightSet(self, height)
      local content = self.content
      local contentheight = height - 34
      if contentheight < 0 then
         contentheight = 0
      end
      content:SetHeight(contentheight)
      content.height = contentheight
   end
   
   local function Constructor()
      local frame = CreateFrame("Frame",nil,UIParent)
      local self = {}
      self.type = Type
      
      self.Hide = Hide
      self.Show = Show
      self.OnRelease = OnRelease
      self.OnAcquire = OnAcquire
      self.ApplyStatus = ApplyStatus
      self.OnWidthSet = OnWidthSet
      self.OnHeightSet = OnHeightSet
      
      self.localstatus = {}
      
      self.frame = frame
      frame.obj = self

      frame:SetWidth(100)
      frame:SetHeight(100)
      frame:SetPoint("CENTER",UIParent,"CENTER",0,0)
      frame:EnableMouse()
      frame:SetMovable(true)
      frame:SetFrameStrata("FULLSCREEN_DIALOG")
      frame:SetScript("OnMouseDown", frameOnMouseDown)
      frame:SetScript("OnMouseUp", frameOnMouseUp)
      
      frame:SetBackdrop(FrameBackdrop)
      frame:SetBackdropColor(0,0,0,1)
      frame:SetScript("OnHide",frameOnClose)
      frame:SetToplevel(true)
      
      --Container Support
      local content = CreateFrame("Frame",nil,frame)
      self.content = content
      content.obj = self
      content:SetPoint("TOPLEFT",frame,"TOPLEFT",17,-17)
      content:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-17,17)
      
      AceGUI:RegisterAsContainer(self)
      return self    
   end
   
   AceGUI:RegisterWidgetType(Type,Constructor,Version)
end






local gui

local OnHyperlinkClick
local SelectMobType
local SetFormattedText
local CreateMobList
local RangeFromVariance

local selectedMobType

function SetFormattedText(self, text, ...)
	self:SetText(format(text or "", ...))
end

function RangeFromVariance(n, avg, totalSqr)
	-- Estimate the variance s. Formulas taken from http://en.wikipedia.org/wiki/Variance, I don't really know what I'm doing.
	local s
	if n>1 then
		s = totalSqr/(n-1) - n/(n-1)*avg*avg
	else
		s = totalSqr/n - avg*avg
	end
	-- A random variable in [1,m] has variance s=(m*m-1)/12.
	local m = sqrt(12*s + 1)
	return avg-m/2, avg+m/2
end

function SelectMobType(_, _, mobType)
	if not mobType then return end
	selectedMobType = mobType
	--gui:SetTitle(mobdb[mobType].name)

	local sf = gui.data
	sf:ReleaseChildren()
	local typeLabel = AceGUI:Create('Label')
	SetFormattedText(typeLabel, "Mob type ID %i=0x%X", mobType, mobType)
	sf:AddChild(typeLabel)

	do
		local inline = AceGUI:Create('InlineGroup')
		inline:SetTitle("Abilities")
		inline.width = "fill"
		local html = AceGUI:Create("SimpleHTML")
		html.width = 'fill'
		html.frame:SetScript('OnHyperlinkClick', OnHyperlinkClick)
		local textHeight = 10
		local height = 0
		local text = "<html><body>"
		local spells = {}
		for spellID in pairs(mobdb[mobType].abilities) do
			tinsert(spells, spellID)
		end
		sort(spells, function(a,b) return (GetSpellInfo(a)) < (GetSpellInfo(b)) end)
		for _,spellID in ipairs(spells) do
			local link = GetSpellLink(spellID)
			-- "Death & Decay" contains an ampersand...
			link = gsub(link, '&', '&amp;')
			text = text..'<p>'..link..'</p>'
			height = height + textHeight
		end
		text = text..'</body></html>'
		html:SetText(text)
		html:SetHeight(height)
		inline:AddChild(html)

		sf:AddChild(inline)
	end

	-- mobdb[GetMobType(srcGUID)].level[srcLevel] .melee [apmod]
	for lvl, data in pairs(mobdb[mobType].level) do
		local inline = AceGUI:Create('InlineGroup')
		inline:SetLayout("List")
		inline:SetTitle(format("Level %i", lvl))
		inline.width = "fill"

		for apmod, dmg in pairs(data.melee) do
			local html = AceGUI:Create("Label")
			--html.frame:SetScript('OnHyperlinkClick', OnHyperlinkClick)
			html.width = 'fill'
			local avg = dmg.total/dmg.num
			local varmin, varmax = RangeFromVariance(dmg.num, avg, dmg.totalSqr)
			local formatstring = "%ix[%.0f-%.0f], [%.0f-%.0f], avg %.1f"
			if type(apmod)=='number' then 
				if apmod~=0 then
					formatstring = formatstring..", AP %+i"
				end
			else
				local id = {strsplit("+", apmod)}
				-- the first is always the ap modifier
				apmod = tostring(id[1])
				for i=2,#id do
					local link = GetSpellLink(id[i])
					if link then
						apmod = apmod..'+'..link
					else
						apmod = apmod..'+'..id[i]
					end
				end
				formatstring = formatstring..", AP %s"
			end
			SetFormattedText(html, formatstring, dmg.num, dmg.min, dmg.max, varmin, varmax, avg, apmod)
			inline:AddChild(html)
		end
		sf:AddChild(inline)
	end

	local delete = AceGUI:Create('Button')
	delete:SetText('Remove data for '..mobdb[mobType].name)
	delete:SetCallback('OnClick', function() 
		OneHitWonder:Printf('removing %s', mobdb[mobType].name)
		mobdb[mobType] = nil
		CreateMobList()
		sf:ReleaseChildren()
	end)
	sf:AddChild(delete)
end

-- Keeping the mob list up-to-date.
-- The list doesn't need to be touched if the GUI is not shown.
function CreateMobList()
	local tree = {}
	local filter = gui.filter:lower()
	local level = tonumber(filter)
	if level then
		for mobType,mob in pairs(mobdb) do
			if mob.level[level] then
				tinsert(tree, {value=mobType, text=mob.name})
			end
		end
	elseif filter:len() >= 3 then
		for mobType,mob in pairs(mobdb) do
			if mob.name:lower():find(filter) then
				tinsert(tree, {value=mobType, text=mob.name})
			end
		end
	else
		for mobType,mob in pairs(mobdb) do
			tinsert(tree, {value=mobType, text=mob.name})
		end
	end
	sort(tree, function(a,b) return a.text<b.text end)
	gui.moblist:SetTree(tree)
	gui.moblist.userdata.invalid = nil
end

function OneHitWonder:UpdateMobList()
	if not gui then return end
	if gui:IsShown() then
		CreateMobList()
	else
		gui.moblist.userdata.invalid = true
	end
end

local OnShow
function OnShow(frame)
	if frame ~= gui.frame then
		frame:SetScript('OnShow')
		return
	end
	if gui.moblist.userdata.invalid then
		CreateMobList()
	end
end

local function OnFilterChanged(frame, event, text)
	if gui.filter ~= text then
		gui.filter = text
		CreateMobList()
	end
end

-- /run OneHitWonder:CreateGUI()
function OneHitWonder:CreateGUI()
	if gui then
		return
	end
	mobdb = OneHitWonder.db.global.mobdb
	local f = AceGUI:Create("Frame")
	gui=f
	self.gui = gui
	f:SetTitle("Mob Stats")
	f:SetStatusText("")
	f:SetLayout("Flow")
	f:SetWidth(500)
	f:SetHeight(400)
	f.frame:SetPoint('RIGHT',0,0)
	f.frame:SetScript('OnShow', OnShow)

		local filter = AceGUI:Create("EditBox")
		gui.filter = ''
		filter:SetLabel('Filter')
		filter.width = 'fill'
		filter:SetCallback('OnTextChanged', OnFilterChanged)
		f:AddChild(filter)

		local t = AceGUI:Create("TreeGroup")
		t:SetLayout("Fill")
		--t:SetWidth(200)
		t.width = 'fill'
		t.height = 'fill'
		--t:SetHeight(200)
		t:SetCallback("OnGroupSelected", SelectMobType )
		gui.moblist = t
		self:UpdateMobList()
		f:AddChild(t)

			local sf = AceGUI:Create("ScrollFrame")
			sf:SetLayout("List")
			t:AddChild(sf)
			f.data = sf
end

do
	local spellDropdown = CreateFrame("Frame", "OneHitWonderSpellDropdown", nil, "UIDropDownMenuTemplate")
	spellDropdown.point = "CENTER"
	spellDropdown.displayMode = "MENU"
	spellDropdown.relativePoint = "TOPRIGHT"

	function OneHitWonder:RemoveSelfBuff(mobType, spellID)
		if not mobdb[mobType] then return end
		self:Printf('removing %s as damage buff from %s.', (GetSpellInfo(spellID)), mobdb[mobType].name)

		-- We need to iterate over each level's melee data.
		for level, lvldata in pairs(mobdb[mobType].level) do
			-- If the data's key contains the spell, we need to remove it.
			-- If the modified key is already present, we need to merge the data.
			-- Ex. Foror Winterchill, he has this Frost Armor buff.
			-- Then we need to merge the entries for apmod == '-300+31256' with those where apmod == -300.
			-- Modifying a table while looping over it is not a good idea. Thus we start
			-- by constructing a map oldkey->newkey.
			local keymap = {}
			for apmod, apdata in pairs(lvldata.melee) do
				local id = {strsplit("+", apmod)}
				-- the first is always the ap modifier
				local spellString = tostring(spellID)
				local foundit
				for i=2,#id do
					if id[i]==spellString then
						foundit = true
						table.remove(id,i)
						-- construct the new key
						if #id==1 then
							keymap[apmod] = tonumber(id[1])
						else
							keymap[apmod] = strjoin('+', unpack(id))
						end
					end
				end
			end
			-- Now merge/move the data.
			for oldkey, newkey in pairs(keymap) do
				if lvldata.melee[newkey] then
					self:Printf('merging %s with %s', oldkey, tostring(newkey))
					local new = lvldata.melee[newkey]
					local old = lvldata.melee[oldkey]
					new.min = math.min(new.min, old.min)
					new.max = math.max(new.max, old.max)
					new.num = new.num+old.num
					new.total = new.total+old.total
					new.totalSqr = new.totalSqr+old.totalSqr
					new.school = new.school or old.school
					new.cancrush = new.cancrush or old.cancrush
				else
					self:Printf('mapping %s to %s', oldkey, tostring(newkey))
					lvldata.melee[newkey] = lvldata.melee[oldkey]
				end
				lvldata.melee[oldkey] = nil
			end
		end
		mobdb[mobType].abilities[spellID] = 'ignore'
		SelectMobType(nil,nil,mobType)
	end

	local removedBuff
	local menu = {
		{
			text = "Remove as damage buff",
			tooltipTitle = 'Remove as damage buff',
			tooltipText = 'If this spell is a self-buff, removing it will merge all melee data where this buff was active with the data where this buff was not active. Applications of the buff will be ignored in the future. \nTo be used for buffs like Frenzy, Renew, Frost Armor, etc. Not undoable unless you delete all data for this mob type. (Alt+F4 to cancel ^^)',
			--disabled = true,
			func = function() OneHitWonder:RemoveSelfBuff(selectedMobType, removedBuff) end,
		},
	}

	function OnHyperlinkClick(self, link, text, button)
		if button == 'RightButton' then
			spellDropdown.relativeTo = self
			local spellID = tonumber(link:sub(7,-1))
			removedBuff = spellID
			menu[1].disabled = mobdb[selectedMobType].abilities[spellID] == 'ignore'

			OneHitWonder:Print(link, text)

			EasyMenu(menu, spellDropdown)
		else
			ChatFrame_OnHyperlinkShow(link, text, button)
		end
	end
end

function OneHitWonder:ShowStats(mobType)
	self:CreateGUI()
	if type(mobType)=='number' then
		gui.moblist:SelectByValue(mobType)
		--SelectMobType(nil,nil, mobType)
	end

	gui:Show()
end

function OneHitWonder:ShowDBBrowser()
	self:ShowStats(GetMobType(UnitGUID('target')))
end

function OneHitWonder:InitializeGUI()
	mobdb = OneHitWonder.db.global.mobdb
end

local stats
do
	--[[
	TODOs
	save position
	--]]
	stats = AceGUI:Create('SimpleFrame')
	OneHitWonder.stats = stats
	stats:SetWidth(150)
	stats:SetHeight(120)
	stats:SetLayout("List")
	stats:Hide()

	local apmodLabel = AceGUI:Create("Label")
	stats:AddChild(apmodLabel)
	local modLabel = AceGUI:Create("Label")
	stats:AddChild(modLabel)
	local rangeLabel = AceGUI:Create("Label")
	stats:AddChild(rangeLabel)
	local rangeLabel2 = AceGUI:Create("Label")
	stats:AddChild(rangeLabel2)
	local avgLabel = AceGUI:Create("Label")
	stats:AddChild(avgLabel)
	local numLabel = AceGUI:Create("Label")
	stats:AddChild(numLabel)
	local numHitsLabel = AceGUI:Create("Label")
	stats:AddChild(numHitsLabel)
	local timeLeftLabel = AceGUI:Create("Label")
	stats:AddChild(timeLeftLabel)

	-- Time in s we can survive w/o heal after the next swing occurs.
	local timeLeft = 0
	-- Time when the next swing will occur.
	local nextSwing = 0
	local scriptSet

	function OneHitWonder:UpdateTargetStats()
		self:ShowTargetStats()
	end
	local playerHealth = 100
	local QuickHealth = LibStub('LibQuickHealth-1.0')
	if QuickHealth then
		function OneHitWonder:OnHealthUpdated(event, GUID, newHealth)
			--self:Print(event, GUID, newHealth)
			if GUID==UnitGUID('player') then
				playerHealth=newHealth
				--self:Printf('new health %i', playerHealth)
				self:UpdateTimeLeft()
			end
		end
		QuickHealth.RegisterCallback(OneHitWonder, "HealthUpdated", 'OnHealthUpdated')
	end

	function OneHitWonder:UpdatePlayerHealth()
		--if not stats:IsVisible() then return end
		local hp = UnitHealth('player')
		if hp ~= playerHealth then
			playerHealth = hp
			--self:Printf('new UnitHealth %i', playerHealth)
			self:UpdateTimeLeft()
		end
	end

	-- Computes the time left until death.
	-- Depends on:
	-- player health
	-- mob max/avg damage
	-- mob swing timer
	function OneHitWonder:UpdateTimeLeft()
		local max = stats.maxhit
		if not max then return end
		local avg = stats.avghit
		local hp = playerHealth
		timeLeft = 0
		if hp>max then
			-- Let's say we have max+100 HP, and avg is 50. That means we have <=max HP after 2 hits.
			hp = hp-max
			local numhits = math.ceil(hp/avg)
			local speed = UnitAttackSpeed('target')
			timeLeft = speed*numhits
			numHitsLabel:SetText(tostring(numhits))
		else
			numHitsLabel:SetText('0')
		end

		self:UpdateETD()
	end

	function OneHitWonder:OnTargetAttackSpeed()
	end

	function OneHitWonder:OnTargetParry(timestamp)
		local parryTime = GetTime()--TODO
		-- Compute swing cooldown.
		local cooldown = nextSwing-parryTime
		if cooldown<0 then
			-- Swing not on cooldown.
			return 
		end
		local speed = UnitAttackSpeed('target')
		local p = cooldown/speed
		local p2
		if p>0.6 then
			p = p-0.4
		elseif p>0.2 then
			p = 0.2
		else
			return
		end
		cooldown = p*speed
		nextSwing = cooldown+parryTime
		self:UpdateETD()
	end

	function OneHitWonder:OnTargetSwing(timestamp)
		local swingTime = GetTime()--TODO
		nextSwing = swingTime+UnitAttackSpeed('target')
		if not scriptSet then
			stats.frame:SetScript('OnUpdate', OneHitWonder.UpdateETD)
			scriptSet = true
		end
		self:UpdateETD()
	end

	function OneHitWonder:UpdateETD()
		local now = GetTime()
		local left
		if now < nextSwing then
			left = timeLeft + nextSwing-now
		else
			-- The target swing is not on CD.
			left = timeLeft
			if scriptSet then
				stats.frame:SetScript('OnUpdate', nil)
				scriptSet = nil
			end
		end
		timeLeftLabel:SetText(f('%.1f = %.1f + %.1fs', left, timeLeft, left-timeLeft))
	end


	function OneHitWonder:ShowTargetStats()
		local guid = UnitGUID('target')
		if not guid then return end
		local mobType = GetMobType(guid)
		if not mobType then return end

		local level = UnitLevel('target')
		level = level==-1 and 73 or level
		if not mobdb[mobType] or not mobdb[mobType].level[level] then return end

		local apmod = OneHitWonder:MobAPModifier(guid)
		if not apmod then
			apmodLabel:SetText('AP mod unknown')
		else
			apmodLabel:SetText('AP mod '..tostring(apmod))
		end
		local melee = mobdb[mobType].level[level].melee[apmod]
		local msg
		if not melee then
			msg = 'no data'
		elseif not melee.school then
			-- TODO remove
			msg = 'school unknown'
		else
			local num = melee.num
			local min, max, avg = melee.min, melee.max, melee.total/num
			local varmin, varmax = RangeFromVariance(num, avg, melee.totalSqr)
			local modifier = self:GetDamageMultipliers(level, melee.school)

			rangeLabel:SetText(f('[%.1f, %.1f]', min*modifier, max*modifier))
			rangeLabel2:SetText(f('[%.1f, %.1f]', varmin*modifier, varmax*modifier))
			avgLabel:SetText(f('avg %.1f', avg*modifier))
			modLabel:SetText(f('mod %+4.1f%%', (modifier-1)*100))
			numLabel:SetText(f('%i hits', num))
			if num > 50 then
				stats.maxhit = varmax*modifier
			else
				stats.maxhit = max*modifier
			end
			stats.avghit = math.floor(avg*modifier + .5)
			self:UpdateTimeLeft()
		end

		if msg then
			rangeLabel:SetText(msg)
			rangeLabel2:SetText('')
			avgLabel:SetText('')
			modLabel:SetText('')
			numLabel:SetText('')
			numHitsLabel:SetText('')
			stats.maxhit = nil
		end

		stats:Show()
	end

	function OneHitWonder:HideTargetStats()
		stats.maxhit = nil
		stats:Hide()
	end
end
